import * as THREE from 'three';
import {FullScreenQuad, Pass} from 'three/examples/jsm/postprocessing/Pass';
import {LuminosityHighPassShader} from "three/examples/jsm/shaders/LuminosityHighPassShader";
import {CopyShader} from "three/examples/jsm/shaders/CopyShader";
import {UnrealBloomMaterial} from "./UnrealBloomMaterial";

export class MyUnrealBloomPass extends Pass {

  private clearColor;
  private renderTargetsHorizontal;
  private renderTargetsVertical;
  private nMips = 5;
  private renderTargetBright: THREE.WebGLRenderTarget;
  private highPassUniforms;
  private materialHighPassFilter;
  private separableBlurMaterials;
  private compositeMaterial;
  private bloomTintColors;
  private copyUniforms;
  private materialCopy;
  private _oldClearColor;
  private oldClearAlpha;
  private fsQuad;
  private basic: THREE.MeshBasicMaterial;

  constructor( private resolution, public strength, public radius, public threshold ) {

    super();
    this.strength = strength !== undefined ? strength : 1;
    this.radius = radius;
    this.threshold = threshold;
    this.resolution = resolution !== undefined ? new THREE.Vector2( resolution.x, resolution.y ) : new THREE.Vector2( 256, 256 ); // create color only once here, reuse it later inside the render function

    this.clearColor = new THREE.Color( 0, 0, 0 ); // render targets

    this.renderTargetsHorizontal = [];
    this.renderTargetsVertical = [];
    this.nMips = 5;
    let resx = Math.round( this.resolution.x / 2 );
    let resy = Math.round( this.resolution.y / 2 );
    this.renderTargetBright = new THREE.WebGLRenderTarget( resx, resy );
    this.renderTargetBright.texture.name = 'UnrealBloomPass.bright';
    this.renderTargetBright.texture.generateMipmaps = false;

    for ( let i = 0; i < this.nMips; i ++ ) {

      const renderTargetHorizonal = new THREE.WebGLRenderTarget( resx, resy );
      renderTargetHorizonal.texture.name = 'UnrealBloomPass.h' + i;
      renderTargetHorizonal.texture.generateMipmaps = false;
      this.renderTargetsHorizontal.push( renderTargetHorizonal );
      const renderTargetVertical = new THREE.WebGLRenderTarget( resx, resy );
      renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
      renderTargetVertical.texture.generateMipmaps = false;
      console.log(renderTargetVertical);
      this.renderTargetsVertical.push( renderTargetVertical );
      resx = Math.round( resx / 2 );
      resy = Math.round( resy / 2 );

    } // luminosity high pass material


    if ( LuminosityHighPassShader === undefined ) console.error( 'THREE.UnrealBloomPass relies on THREE.LuminosityHighPassShader' );
    const highPassShader = LuminosityHighPassShader;
    this.highPassUniforms = THREE.UniformsUtils.clone( highPassShader.uniforms );
    this.highPassUniforms[ 'luminosityThreshold' ].value = threshold;
    this.highPassUniforms[ 'smoothWidth' ].value = 0.01;
    this.materialHighPassFilter = new THREE.ShaderMaterial( {
      uniforms: this.highPassUniforms,
      vertexShader: highPassShader.vertexShader,
      fragmentShader: highPassShader.fragmentShader,
      defines: {}
    } ); // Gaussian Blur Materials

    this.separableBlurMaterials = [];
    const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
    resx = Math.round( this.resolution.x / 2 );
    resy = Math.round( this.resolution.y / 2 );

    for ( let i = 0; i < this.nMips; i ++ ) {

      this.separableBlurMaterials.push( this.getSeperableBlurMaterial( kernelSizeArray[ i ] ) );
      this.separableBlurMaterials[ i ].uniforms[ 'texSize' ].value = new THREE.Vector2( resx, resy );
      resx = Math.round( resx / 2 );
      resy = Math.round( resy / 2 );

    } // Composite material


    // this.compositeMaterial = new THREE.MeshBasicMaterial({map: this.renderTargetsVertical[ 0 ].texture});
    this.compositeMaterial = this.getCompositeMaterial( this.nMips );
    // this.compositeMaterial = new UnrealBloomMaterial(this.nMips);
    this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture;
    this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture;
    this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture;
    this.compositeMaterial.uniforms[ 'blurTexture4' ].value = this.renderTargetsVertical[ 3 ].texture;
    this.compositeMaterial.uniforms[ 'blurTexture5' ].value = this.renderTargetsVertical[ 4 ].texture;
    this.compositeMaterial.uniforms[ 'bloomStrength' ].value = strength;
    this.compositeMaterial.uniforms[ 'bloomRadius' ].value = 0.1;
    this.compositeMaterial.needsUpdate = true;
    const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ];
    this.compositeMaterial.uniforms[ 'bloomFactors' ].value = bloomFactors;
    this.bloomTintColors = [ new THREE.Vector3( 1, 1, 1 ), new THREE.Vector3( 1, 1, 1 ), new THREE.Vector3( 1, 1, 1 ), new THREE.Vector3( 1, 1, 1 ), new THREE.Vector3( 1, 1, 1 ) ];
    this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors; // copy material

    if ( CopyShader === undefined ) {

      console.error( 'THREE.UnrealBloomPass relies on THREE.CopyShader' );

    }

    const copyShader = CopyShader;
    this.copyUniforms = THREE.UniformsUtils.clone( copyShader.uniforms );
    this.copyUniforms[ 'opacity' ].value = 1.0;
    this.materialCopy = new THREE.ShaderMaterial( {
      uniforms: this.copyUniforms,
      vertexShader: copyShader.vertexShader,
      fragmentShader: copyShader.fragmentShader,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      depthWrite: false,
      transparent: true
    } );
    this.enabled = true;
    this.needsSwap = false;
    this._oldClearColor = new THREE.Color();
    this.oldClearAlpha = 1;
    this.basic = new THREE.MeshBasicMaterial();
    this.fsQuad = new FullScreenQuad( null );

  }

  dispose() {

    for ( let i = 0; i < this.renderTargetsHorizontal.length; i ++ ) {

      this.renderTargetsHorizontal[ i ].dispose();

    }

    for ( let i = 0; i < this.renderTargetsVertical.length; i ++ ) {

      this.renderTargetsVertical[ i ].dispose();

    }

    this.renderTargetBright.dispose();

  }

  // @ts-ignore
  setSize( width, height ) {

    let resx = Math.round( width / 2 );
    let resy = Math.round( height / 2 );
    this.renderTargetBright.setSize( resx, resy );

    for ( let i = 0; i < this.nMips; i ++ ) {

      this.renderTargetsHorizontal[ i ].setSize( resx, resy );
      this.renderTargetsVertical[ i ].setSize( resx, resy );
      this.separableBlurMaterials[ i ].uniforms[ 'texSize' ].value = new THREE.Vector2( resx, resy );
      resx = Math.round( resx / 2 );
      resy = Math.round( resy / 2 );

    }

  }

  // @ts-ignore
  render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {

    renderer.getClearColor( this._oldClearColor );
    this.oldClearAlpha = renderer.getClearAlpha();
    const oldAutoClear = renderer.autoClear;
    renderer.autoClear = false;
    renderer.setClearColor( this.clearColor, 0 );
    if ( maskActive ) renderer.state.buffers.stencil.setTest( false ); // Render input to screen

    if ( this.renderToScreen ) {

      this.fsQuad.material = this.basic;
      this.basic.map = readBuffer.texture;
      renderer.setRenderTarget( null );
      renderer.clear();
      this.fsQuad.render( renderer );

    } // 1. Extract Bright Areas


    this.highPassUniforms[ 'tDiffuse' ].value = readBuffer.texture;
    this.highPassUniforms[ 'luminosityThreshold' ].value = this.threshold;
    this.fsQuad.material = this.materialHighPassFilter;
    renderer.setRenderTarget( this.renderTargetBright );
    renderer.clear();
    this.fsQuad.render( renderer ); // 2. Blur All the mips progressively

    let inputRenderTarget = this.renderTargetBright;

    for ( let i = 0; i < 5; i ++ ) {

      this.fsQuad.material = this.separableBlurMaterials[ i ];
      this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture;
      this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = BlurDirectionX;
      // this.separableBlurMaterials[ i ].uniforms[ 'directionY' ].value = BlurDirectionY;
      renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
      renderer.clear();
      this.fsQuad.render( renderer );
      this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture;
      this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = BlurDirectionY;
      renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
      renderer.clear();
      this.fsQuad.render( renderer );
      inputRenderTarget = this.renderTargetsVertical[ i ];

    } // Composite All the mips


    this.fsQuad.material = this.compositeMaterial;
    this.compositeMaterial.uniforms[ 'bloomStrength' ].value = this.strength;
    this.compositeMaterial.uniforms[ 'bloomRadius' ].value = this.radius;
    this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
    renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] );
    renderer.clear();
    this.fsQuad.render( renderer ); // Blend it additively over the input texture

    this.fsQuad.material = this.materialCopy;
    this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetsHorizontal[ 0 ].texture;
    if ( maskActive ) renderer.state.buffers.stencil.setTest( true );

    if ( this.renderToScreen ) {

      renderer.setRenderTarget( null );
      this.fsQuad.render( renderer );

    } else {

      renderer.setRenderTarget( readBuffer );
      this.fsQuad.render( renderer );

    } // Restore renderer settings


    renderer.setClearColor( this._oldClearColor, this.oldClearAlpha );
    renderer.autoClear = oldAutoClear;

  }

  getSeperableBlurMaterial( kernelRadius ) {

    return new THREE.ShaderMaterial( {
      defines: {
        'KERNEL_RADIUS': kernelRadius,
        'SIGMA': kernelRadius
      },
      uniforms: {
        'colorTexture': {
          value: null
        },
        'texSize': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'direction': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'directionX': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'directionY': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'colorTexture1': {
          value: null
        },
        'texSize1': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'direction1': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'colorTexture2': {
          value: null
        },
        'texSize2': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'direction2': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'colorTexture3': {
          value: null
        },
        'texSize3': {
          value: new THREE.Vector2( 0.5, 0.5 )
        },
        'direction3': {
          value: new THREE.Vector2( 0.5, 0.5 )
        }
      },
      vertexShader: `varying vec2 vUv;
				void main() {
					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
				}`,
      fragmentShader: `#include <common>
				varying vec2 vUv;
				uniform sampler2D colorTexture;
				uniform vec2 texSize;
				uniform vec2 direction;
				uniform vec2 directionX;
				uniform vec2 directionY;

				uniform sampler2D colorTexture1;
				uniform vec2 texSize1;
				uniform vec2 direction1;

				uniform sampler2D colorTexture2;
				uniform vec2 texSize2;
				uniform vec2 direction2;

				uniform sampler2D colorTexture3;
				uniform vec2 texSize3;
				uniform vec2 direction3;

				float gaussianPdf(in float x, in float sigma) {
					return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
				}

				vec4 computeFuck(in sampler2D _colorTexture, in vec2 _direction) {
       vec2 invSize = 1.0 / texSize;
       float fSigma = float(SIGMA);
       float weightSum = gaussianPdf(0.0, fSigma);
       vec3 diffuseSum = texture2D( _colorTexture, vUv).rgb * weightSum;
       for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
						float x = float(i);
						float w = gaussianPdf(x, fSigma);
						vec2 uvOffset = _direction * invSize * x;
						vec3 sample1 = texture2D( _colorTexture, vUv + uvOffset).rgb;
						vec3 sample2 = texture2D( _colorTexture, vUv - uvOffset).rgb;
						diffuseSum += (sample1 + sample2) * w;
						weightSum += 2.0 * w;
					}
					return vec4(diffuseSum/weightSum, 1.0);
				}

				void main() {
					gl_FragColor = computeFuck(colorTexture, direction);
				}`

      // gl_FragColor = texture2D( colorTexture, vUv);
      /**
       vec2 invSize = 1.0 / texSize;
       float fSigma = float(SIGMA);
       float weightSum = gaussianPdf(0.0, fSigma);
       vec3 diffuseSum = texture2D( colorTexture, vUv).rgb * weightSum;
       for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
						float x = float(i);
						float w = gaussianPdf(x, fSigma);
						vec2 uvOffset = direction * invSize * x;
						vec3 sample1 = texture2D( colorTexture, vUv + uvOffset).rgb;
						vec3 sample2 = texture2D( colorTexture, vUv - uvOffset).rgb;
						diffuseSum += (sample1 + sample2) * w;
						weightSum += 2.0 * w;
					}
       gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
       */
    } );

  }

  getCompositeMaterial( nMips ) {

    return new THREE.ShaderMaterial( {
      defines: {
        'NUM_MIPS': nMips
      },
      uniforms: {
        'blurTexture1': {
          value: null
        },
        'blurTexture2': {
          value: null
        },
        'blurTexture3': {
          value: null
        },
        'blurTexture4': {
          value: null
        },
        'blurTexture5': {
          value: null
        },
        'bloomStrength': {
          value: 1.0
        },
        'bloomFactors': {
          value: null
        },
        'bloomTintColors': {
          value: null
        },
        'bloomRadius': {
          value: 0.0
        }
      },
      vertexShader: `varying vec2 vUv;
				void main() {
					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
				}`,
      fragmentShader: `
      varying vec2 vUv;
				uniform sampler2D blurTexture1;
				uniform sampler2D blurTexture2;
				uniform sampler2D blurTexture3;
				uniform sampler2D blurTexture4;
				uniform sampler2D blurTexture5;
				uniform float bloomStrength;
				uniform float bloomRadius;
				uniform float bloomFactors[NUM_MIPS];
				uniform vec3 bloomTintColors[NUM_MIPS];

				float lerpBloomFactor(const in float factor) {
					float mirrorFactor = 1.2 - factor;
					return mix(factor, mirrorFactor, bloomRadius);
				}

				void main() {
					gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv)
					 +
						lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
						lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
						lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
						lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv)
						 );
				}`
    }
    );

  }

}

const BlurDirectionX = new THREE.Vector2( 1.0, 0.0 );
const BlurDirectionY = new THREE.Vector2( 0.0, 1.0 );
